/*
 * Copyright 2012 Ralf Ovelgoenne, Q_PERIOR AG
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package com.qperior.gsa.httpproxy;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.configuration.ConfigurationUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.impl.Log4JLogger;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

/**
 * Base class servlet for the http proxy.
 * <p>
 * DoGet gets processed by "processRequest". DoPost is not supported.
 * 
 * @author Ralf Ovelgoenne
 */
public class QPHttpProxyServlet extends HttpServlet
{
	private static final long serialVersionUID = 7090167404840704608L;

	private Log log = (Log4JLogger) LogFactory.getLog(this.getClass());
	
	/**
	 * 
	 */
	protected String webAppBaseURL;
	
	/**
	 * Called by the application server's servlet runner when GET method
	 * requests are made for this servlet. 
	 */
	@Override
	public void doGet (HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		logHeaders(request);
		this.webAppBaseURL = request.getScheme() + "://" +
						request.getServerName() + ":" + request.getServerPort() +
						request.getContextPath() + "/";
		
		processRequest(request, response);
	}
	
	/**
	 * Method 'doPost' is not supported!
	 */
	@Override
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
		String resString = "Method 'doPost' is not supported!";		
		this.log.warn(resString);
		
		// Defines response MIME type
		response.setContentType("text/xml");
		// Wirte response back as XML string
		PrintWriter out = response.getWriter();
		out.print(resString);
		out.close();
	}

	/**
	 * Logging the HTTP headers sent in the request. 
	 * 
	 * @param request HttpServletRequest
	 */
	private void logHeaders(HttpServletRequest request)
	{	
		this.log.info("--------------------------");
		this.log.info("Request-Headers:");
		@SuppressWarnings("unchecked")
		Enumeration<String> headerNames = request.getHeaderNames();
		while (headerNames.hasMoreElements()) {
			String headerName = (String)headerNames.nextElement();
			String headerValue = request.getHeader(headerName);
			this.log.info("["+headerName +" = "+headerValue+"]");
		}
		this.log.info("--------------------------");
	}
	
	/**
	 * Top level request processor for the http proxy.
	 * <p>
	 * The target url 
	 * 
	 * @param request 
	 * @param response 
	 * @throws ServletException 
	 * @throws IOException 
	 */
	protected void processRequest(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{		
		String htmlResult = "";
		boolean isCode = false;
		
		// Get all parameter from properties
		String targetUri = QPProperties.getTargetURI();
		String xslFilePath = QPProperties.getXSLFilePath();
		String errorFilePath = QPProperties.getErrorFilePath();				
		boolean isHttps = QPProperties.isHTTPS();
		boolean isBasicAuthN = QPProperties.isBasicAUTHN();
		
		try {
			
			// put the extry path information to basic target url
			String pathinfo = request.getPathInfo();
			if ( pathinfo != null ) {
				targetUri = targetUri + pathinfo;
			}
			this.log.info("Target URI: " + targetUri);
			
			URL urlLink = new URL(targetUri);
			URI uriFormatted = new URI(urlLink.getProtocol(), urlLink.getHost(), urlLink.getPath(), urlLink.getQuery(), null);
			this.log.info("Target URI (encoded): " + uriFormatted);
			
			HttpGet httpget = new HttpGet(uriFormatted);			
			
			DefaultHttpClient httpclient = new DefaultHttpClient();		
			httpget.addHeader("accept", "application/xml");
			
			// TODO: get User from request
			/* Basic authorization with Base64 encoded user (Base64.encodeBase64String(authn.getBytes()))*/				
			if ( isBasicAuthN ) {
				httpget.addHeader("Authorization", "Basic " + "MTYxMDpyYWxm");
			}

			// with https to everything to avoid error
			if ( isHttps ) {
				this.ensureSsl(httpclient);
			}
			
			// do the http get
			HttpResponse getResponse = httpclient.execute(httpget);
			
			// use status: 200 - OK, otherwise error
			int status = getResponse.getStatusLine().getStatusCode();
			this.log.info("Status: " + status);			
			if ( status == 200 ) {
				
				HttpEntity entity = getResponse.getEntity();
				if (entity != null ) {
					
					// get the XML string from response entity
					String xmlResult = EntityUtils.toString(entity);
					this.log.debug("XML result: " + xmlResult);	
					
					// local XSL file, otherwise load it through URI (systemId)
					// TODO: load XSL file static
					File xslFile = this.locateFile(xslFilePath);
					this.log.debug("Used XSL: " + xslFilePath);
					
					// transform the XML
					try {
						htmlResult = this.transformXMLString(xmlResult, xslFile);
						this.log.info("HTML result used.");
						this.log.debug("HTML: " + htmlResult);		
					} catch (TransformerException texc) {
						// No XML content, then it is code, then use the xmlResult and return it
						this.log.info("Code result used.");
						this.log.debug("Code: " + xmlResult);
						htmlResult = xmlResult;
						isCode = true;
						
					}
				}
			}
		}
		catch (Exception exc ) {
			// with every exception give back an error html
			this.log.error("Unhandled exception in servlet. ", exc);
			this.log.info("Error result used.");
			File file = this.locateFile(errorFilePath);
			htmlResult =  FileUtils.readFileToString(file);
		}
		finally {
			
			// ensure to give back a valid html result
			if ( htmlResult == null || htmlResult.equals("") ) {
				
				File file = this.locateFile(errorFilePath);
				htmlResult =  FileUtils.readFileToString(file);
				this.log.info("Default result used.");
			}
		}
		
		// Defines response MIME type
		if ( isCode ) {
			response.setContentType("text/plain");
		} else {
			response.setContentType("text/html");
		} 
		
		// Write response back as XML string
		PrintWriter out = response.getWriter();
		out.print(htmlResult);
		out.close();
		this.log.info("Target URI '" + targetUri + "' finished.");
	}
	
	/**
	 * Do everything to realize https communication (simplest way)
	 * 
	 * @param httpclient
	 * @throws Exception
	 */
	private void ensureSsl(DefaultHttpClient httpclient) throws Exception {
		
		X509TrustManager tm = new X509TrustManager() {
			 
			public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
			}
			 
			public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
			}
			 
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}
		};
		X509HostnameVerifier verifier = new X509HostnameVerifier() {
			 
            @Override
            public void verify(String string, SSLSocket ssls) throws IOException {
            }

            @Override
            public void verify(String string, X509Certificate xc) throws SSLException {
            }

            @Override
            public void verify(String string, String[] strings, String[] strings1) throws SSLException {
            }

            @Override
            public boolean verify(String string, SSLSession ssls) {
                return true;
            }
        };
		SSLContext ctx = SSLContext.getInstance("TLS");
		ctx.init(null, new TrustManager[]{tm}, null);
		SSLSocketFactory ssf = new SSLSocketFactory(ctx);
		ssf.setHostnameVerifier(verifier);
		ClientConnectionManager ccm = httpclient.getConnectionManager();
		SchemeRegistry sr = ccm.getSchemeRegistry();
		sr.register(new Scheme("https", ssf, 443));
	}
	
	/**
	 * Transform an XML string trough a XSL file
	 * 
	 * @param xmlResult
	 * @param xslFile
	 * @return Transformed string
	 * @throws TransformerException
	 * @throws MalformedURLException
	 */
	private String transformXMLString( String xmlResult, File xslFile ) throws TransformerException, MalformedURLException {
			
		Source xsltSource = new StreamSource(xslFile);
		Source xmlSource = new StreamSource(new StringReader(xmlResult));

		TransformerFactory transFact =
            TransformerFactory.newInstance();
		Transformer trans = transFact.newTransformer(xsltSource);
        
		StringWriter writer = new StringWriter();
		trans.transform(xmlSource, new StreamResult(writer));
        
		return writer.toString();		
	}
	
	/**
	 * Locate a file in the classpath
	 * 
	 * @param pathInClasspath
	 * @return File
	 */
	private File locateFile( String pathInClasspath ) {
		
		URL url = ConfigurationUtils.locate(null, pathInClasspath);
		return FileUtils.toFile(url);
	}
	
	/**
	 * Just for testing!
	 * 
	 * @param args
	 */
	public static void main (String[] args ) {
		
		try {
			
			QPHttpProxyServlet servlet = new QPHttpProxyServlet();
			
			String targetUri = "https://test.de/ka_inver - 25012012.xslt";

			URL urlLink = new URL(targetUri);
			URI uriFormatted = new URI(urlLink.getProtocol(), urlLink.getHost(), urlLink.getPath(), urlLink.getQuery(), null);
			
			HttpGet httpget = new HttpGet(uriFormatted);			
			
			DefaultHttpClient httpclient = new DefaultHttpClient();		
			httpget.addHeader("accept", "application/xml");
			httpget.addHeader("Authorization", "Basic " + "MTYxMDpyYWxm");

			servlet.ensureSsl(httpclient);
			HttpResponse getResponse = httpclient.execute(httpget);
			
			int status = getResponse.getStatusLine().getStatusCode();		
			if ( status == 200 ) {
				
				HttpEntity entity = getResponse.getEntity();
				if (entity != null ) {
					
					// get the XML string from response entity
					String xmlResult = EntityUtils.toString(entity);
				}
			}
			
			/*			
			QPHttpProxyServlet servlet = new QPHttpProxyServlet();
			
			File file = servlet.locateFile("test.xml");
			String xmlResult =  FileUtils.readFileToString(file);	
			
			/*File xsltFile = servlet.locateFile("svn.xsl");
			
			String html = servlet.transformXMLString(xmlResult, xsltFile);/
			Source xsltSource = new StreamSource(xslSystemId);
	        Source xmlSource = new StreamSource(new StringReader(xmlResult));

	        TransformerFactory transFact =
	                TransformerFactory.newInstance();
	        Transformer trans = transFact.newTransformer(xsltSource);
	        
	        StringWriter writer = new StringWriter();
	        trans.transform(xmlSource, new StreamResult(writer));
	        
	        String html = writer.toString();
			
			System.out.println(html);*/
		} 
		catch ( Exception exc ) {
			
			System.out.println(exc.getLocalizedMessage());
		}
	}
}
